<?php
/*

	Форм-процессор:
	* генерация, обработка и валидация простых и сложных форм
	* сохранение в БД и чтение оттуда
	* see http://in.jetstyle.ru/rocket/rocketforms

	Управляющий класс.

	Form( &$rh )
			- $rh              -- ссылка на RH, как обычно

	-------------------
	// Доступ к полям
	Form::hash['fieldname']->model->model_data - содержимое поля

	// Конструирование формы

	* &AddField( $field_name, $config ) - Привязать поле в форму. Делает весь handshaking с созданием поля
			- $field_name -- что за поле
			- $config     -- конфигурация, ага.
	* &_AddField( &$field_object ) - Привязать поле, существующее как объект

	* AddButton( $button_config ) - зарегистрировать кнопку
			- $button_config -- массив-конфиг кнопки

	* _RegisterField( &$field ) - записывает в $form->hash[$field->name] ссылку на это поле.
																НЕ ДЛЯ ВНЕШНЕГО ИСПОЛЬЗОВАНИЯ

	// Процессинг формы !! Самое главное

	* Handle( $ignore_post=false, $ignore_load=false, $ignore_validator=false, $ignore_session=false ) 
			- $ignore_* -- игнорировать те или иные этапы сценария процессинга
			- false, если результат -- не отпарсенная форма (иными словами, если нет редиректа, но событие произошло

	// Настройка формы

	- AssignId( $data_id ) - ставит форме в соответствие строку в БД

	* Load( $data_id=NULL ) - загружает форму из БД
			- $data_id -- если NULL, то берёт this->data_id

	* LoadFromArray( $a ) - загружает форму из массива
			- $a -- массив, из которого загружать

	* Reset() - Сбрасывает форму в "незаполненное" состояние

	// Изменение в БД

	- DbDelete( $data_id=NULL ) -- удаляет соотв. строку из БД,
			- true, if success
			- прежде удаления строки должно вызвать DbDelete всех полей
	- DbInsert()                -- вставляет текущее состояние формы в БД, возвращает $data_id
	- DbUpdate( $data_id=NULL ) -- исправляет строку в БД, возвращает $data_id
	- _DbUpdate( &$fields, &$values ) -- формирует sql-запрос и отправляет его в БД
	- _DbAuto( &$fields, &$values ) -- добавляет в $fields, $values автоматически сгенерированные поля

	// Парсинг, иногда можно отдельно пользовать

	* Parse()
	* ParsePreview()

	// Вспомогательные методы

	* StaticDefaults( $default_config, &$supplied_config ) - статичный метод, модифицирует 
																														supplied_config по дефолтному 
																														(выставляя все поля, которые 
																														отсутствуют в супплиеде
	* _ParseWrapper( $content )
	* _ParseButtons()


================================================================== v.0 (kuso@npj)
 */
define( "FORM_EVENT_OK",     "ok");     // ничего не делаем, переход по "success_url", if redirect  
define( "FORM_EVENT_CANCEL", "cancel"); // отмена, переход по "cancel_url", if redirect  
define( "FORM_EVENT_RESET",  "reset");  // сброс состояния формы к стартовому  
define( "FORM_EVENT_INSERT", "insert"); // вставка в БД, переход по "success_url", if redirect  
define( "FORM_EVENT_UPDATE", "update"); // правка в БД, переход по "success_url", if redirect  
define( "FORM_EVENT_DELETE", "delete"); // удалить всё из БД, переход по "success_url", if redirect  
define( "FORM_EVENT_AUTO",   "auto");   // insert/update based on $data_id  

class Form
{
	var $form_present_var = "__form_present";
	var $data_id_var = "__form_data_id";
	var $form_name_var = "__form_name";
	var $data_id=0;      // строка, ассоциированная с формой. 0 -- значит нет такой
	var $hash=array();   // очень удобный способ доступа к полям
	var $fields=array(); // очень неудобный способ доступа к полям
	var $buttons=array();// хранилище "кнопок"
	var $action; // куда уходить по посту формы

	var $valid = true; // флаг валидности формы

	var $default_config = array(
		"template_prefix"           =>"_forms/",
		"template_prefix_button"    =>"_forms/buttons:",
		"template_prefix_views"     =>"_forms/views:",
		"template_prefix_wrappers"  =>"_forms/",
		"template_prefix_interface" =>"_forms/",
		"template_prefix_group"     =>"_forms/",
		"template_form"                  =>"form:Form",
		"template_buttonlist"            =>"form:Buttons",
		"multipart"    =>  1,
		"auto_datetime"=>  1,  
		"auto_user_id" =>  1,  
		"id_field"     =>  "id",
		"active_field" =>  "active",
		"event_handlers_type" => "form_events",
		"default_event" => FORM_EVENT_OK,

		"db_ignore" => true,
		"db_table"  => false,
		"fieldname_created_user_id"  => "_created_user_id",
		"fieldname_edited_user_id"   => "_edited_user_id",
		"fieldname_created_datetime" => "_created_datetime",
		"fieldname_edited_datetime"  => "_edited_datetime",

		// [optional] "success_url" => 
		// [optional] "cancel_url" =>
	);

	function Form( &$rh, $form_config=NULL, $action=NULL, $tpl_data=NULL )
	{
		$this->rh = &$rh;
		$this->tpl_data = $tpl_data; // массив шаблонных переменных, если был передан

		$this->rh->UseClass("Form_FormField"); // он нам стопудово понадобится

		if ($action != NULL) $this->action = $rh->base_url.$action;

		if (!$form_config) $form_config = $this->default_config;
		else               Form::StaticDefaults($this->default_config, $form_config);

		$this->config = $form_config;

		// eventhandl.
		$a = array( "on_before_event", "on_after_event" );
		foreach($a as $v)
			if (isset($form_config[$v]) && !is_array($form_config[$v]))
			{
				$this->config[$v] = array();
				$this->config[$v][] = &$form_config[$v];
			}
	}

	// автоматизатор "конфигов по-умолчанию"
	function StaticDefaults( $default_config, &$supplied_config )
	{
		foreach( $default_config as $k=>$v )
			if (!isset($supplied_config[$k])) $supplied_config[$k] = $v;
	}

	// Добавить поле
	function &AddField( $field_name = NULL, $config )
	{
		$f = &new FormField( $this, $field_name, $config );
		return $this->_AddField($f);
	}
	function &_AddField( &$field_object )
	{
		$this->fields[] = &$field_object;
		$field_object->_LinkToForm( $this );
		return $field_object;
	}

	// Добавить кнопку
	function AddButton( $button_config )
	{
		$this->buttons[$button_config["title"]] = $button_config;
	}
	
	function IsFormInPost() {
		if ($_POST[$this->form_name_var]=='form_'.$this->config["db_table"])
			return true; else return false;
	}

	// САМАЯ СТРАШНАЯ ПРОЦЕДУРА --------------------------------------------------------
	function Handle( $ignore_post     =false,  $ignore_load   =false, 
		$ignore_validator=false,  $ignore_session=false, $force_parsing=false )
	{
		$processed = false;

		//инициализация значений полей    
		// if ($this->data_id && !$ignore_load) $this->Load();  // пробуем загрузить
		if ( !$this->data_id || $ignore_load ) $this->Reset(); // устанавливаем default-значения

		if (!$ignore_session) $this->FromSession();

		//пробуем обработать пост
		if (isset($_POST[$this->form_present_var]) && $_POST[$this->form_present_var] && !$ignore_post)
		{
			if ( $this->IsFormInPost() ) // дополнительная проверка для идентификации формы 
			// в случае если на странице несколько форм
			{
				$this->LoadFromPost( $_POST );

				// get event
				$event_name = $_POST["_event"];
				if ($_POST["_event2"])
					$event_name = $_POST["_event2"];

				$event = $this->buttons[$event_name];

				if (!$event) $event = $this->config["default_event"];

				$this->filtrate();

				if ($ignore_validator 
					|| ($event["event"] == FORM_EVENT_CANCEL)
						|| ($event["event"] == FORM_EVENT_RESET)
							|| ($event["event"] == FORM_EVENT_DELETE)
								|| $this->Validate()
							)
				{ 
					$processed = 1;
					if (!$ignore_session) $this->ToSession();

					// before
					$handler = "on_before_event";
					if (isset($this->config[$handler]))
						foreach($this->config[$handler] as $k=>$v)
							if (is_object($this->config[$handler][$k]))
								$this->config[$handler][$k] -> OnBeforeEventForm( $event, $this );
							else // this is !!file!!
							{
								$event_handler = $this->rh->FindScript($this->config["event_handlers_dir"], $v);
								if ($event_handler !== false) include($event_handler);
							}  
					debug("handling event ". $event);
					// event
					$this->HandleEvent( $event );

					// after
					$handler = "on_after_event";
					if (isset($this->config[$handler]))
						foreach($this->config[$handler] as $k=>$v)
							if (is_object($this->config[$handler][$k]))
								$this->config[$handler][$k] -> OnAfterEventForm( $event, $this );
							else // ! NB: this is <file> !
							{
								$event_handler = $this->rh->FindScript($this->config["event_handlers_dir"], $v);
								if ($event_handler !== false) include($event_handler);
							}  

				}

			}

		}

		if (!$processed || $force_parsing) {
			$ae = $this->rh->ctx->tpl->autoescape;
			$this->rh->ctx->tpl->autoescape = false;
			$result = $this->Parse();
			$this->rh->ctx->tpl->autoescape = $ae;
		} else $result = false;
		return $result;
	}

	// сбросить все поля формы в начальное состояние
	function Reset()       
	{
		foreach($this->fields as $field) 
			$field->model->Model_SetDefault();
	}

	// парсинг формы в своём обычном состоянии
	function Parse()
	{
		$result = "";

		foreach($this->fields as $field) {
			$_parsed = $field->Parse();
			$result .= $_parsed;
		}

		return $this->_ParseWrapper( $result );
	}

	// парсинг формы "только для чтения", без кнопок
	function ParsePreview()
	{
		$result = "";
		foreach($this->fields as $field) 
			$result .= $field->Parse( "readonly" );
		return $result;
	}

	// парсить всякое окружение: кнопки там, прочее
	function _ParseWrapper( $content )
	{
		$this->tpl_data["form"] = "<form action=\"".$this->action."\" method=\"POST\" id=\"form_".$this->config["db_table"]."\" accept-charset=\"utf-8\" enctype=\"multipart/form-data\">";	
		$this->tpl_data["form_name"] = 'form_'.$this->config["db_table"];
		$this->tpl_data["form_present"] = $this->form_present_var;
		$this->tpl_data["form_name_var"] = $this->form_name_var;
		$this->tpl_data["form_data_id"] = $this->data_id_var;
		$this->tpl_data["form_data_id_value"] = $this->data_id;
		$this->tpl_data["content"] = $content;
		$this->tpl_data["data_id"] = $this->data_id;
		$this->tpl_data["buttons"] = $this->_ParseButtons();

		$view = $this->rh->views->findByName( $this->config["template_prefix"].$this->config["template_form"] );
		debug( $this->config["template_prefix"].$this->config["template_form"] );
		$view->data = $this->tpl_data;
		return $view->getContent();

	}

	// парсинг кнопок
	function _ParseButtons()
	{
		$result = array();
		foreach( $this->buttons as $button )
		{
			$this->tpl_data['*'] = $button;
			debug($this->config["template_prefix_button"].$button["template"]);
			$view = $this->rh->views->findByName( $this->config["template_prefix_button"].$button["template"] );
			$view->data = $this->tpl_data;
			$result[]["BUTTON"] = $view->getContent();
			unset($view);
		}

		$view = $this->rh->views->findByName( $this->config["template_prefix"].$this->config["template_buttonlist"] );
		$view->data = $this->tpl_data;
		$view->data['buttons'] = $result;
		debug($this->config["template_prefix"].$this->config["template_buttonlist"]);
		return $view->getContent();
	}

	// загрузка из формы
	function LoadFromPost( $post_data )           
	{
		$this->AssignId( $post_data[ $this->data_id_var ] );

		foreach($this->fields as $k=>$field) 
			$this->fields[$k]->LoadFromPost( $post_data );
	}

	// валидация всех полей формы
	function Validate()    
	{ 
		$this->valid = true;
		foreach($this->fields as $k=>$field) 
			$this->valid = $this->fields[$k]->Validate() && $this->valid; // важно, что именно в таком порядке
		return $this->valid;
	}

	function filtrate()    
	{ 
		foreach($this->fields as $k=>$field) 
			$this->fields[$k]->filtrate(); // важно, что именно в таком порядке
		return;
	}
	// ДАМП ФОРМЫ
	function _Dump( $is_error=1 )
	{
		$dump_hash = array();
		foreach( $this->fields as $k=>$field )
			$dump_hash[ $field->name ] = $field->_Dump();

		if ($is_error) $this->rh->debug->Error_R( $dump_hash );
		else           $this->rh->debug->Trace_R( $dump_hash );
	}

	// работа в сессии
	function FromSession() 
	{ 
		$key = "form_".$this->config["db_table"];
		$session_storage = isset($_SESSION[$key]) ? $_SESSION[$key] : "";
		if (!is_array($session_storage)) return; // no session -- no restore
		foreach( $this->fields as $k=>$field )
			$this->fields[$k]->FromSession( $session_storage );
	}
	function ToSession()   
	{
		$session_storage = array();
		foreach( $this->fields as $k=>$field )
			$this->fields[$k]->ToSession( $session_storage );
		$_SESSION[ "form_".$this->config["db_table"] ] = $session_storage;
	}
	function ResetSession()
	{ 
		$_SESSION[ "form_".$this->config["db_table"] ] = "";
	}

	// обработка события, ага (вставка/редактирование)
	function HandleEvent( $event = FORM_EVENT_AUTO ) 
	{
		if (is_array($event)) $_event = $event["event"];
		else                  $_event = $event;

		if ($_event == FORM_EVENT_AUTO)
		{
			if ($this->data_id) $_event = FORM_EVENT_UPDATE;
			else                $_event = FORM_EVENT_INSERT;
		}

		switch( $_event )
		{
		case FORM_EVENT_INSERT:
			$this->DbInsert();
			$this->success   = true;
			$this->processed = true;
			break;
		case FORM_EVENT_UPDATE:
			$this->DbUpdate();
			$this->success   = true;
			$this->processed = true;
			break;
		case FORM_EVENT_DELETE:
			$this->DbDelete();
			$this->success   = true;
			$this->processed = true;
			break;

		case FORM_EVENT_RESET:
			$this->ResetSession();
			$this->Reset();
			$this->ToSession();
			$this->success   = false;
			$this->processed = false; // returning to form again
			break;


		case FORM_EVENT_CANCEL:
			$this->success   = false;
			$this->processed = true;
			break;

		case FORM_EVENT_OK:
		default:               $this->success   = true;
			$this->processed = true;
		} 
		$this->processed_event = $event;

		if (!$this->processed)
			$this->rh->Redirect( $this->rh->ri->Href($this->rh->ri->url) );
		else
		{
			$this->ResetSession(); // если успешно обработана, то сессию выкидываем
		}

		if ($this->processed && !$this->success && isset($this->config["cancel_url"])) // cancel
			$this->rh->Redirect( $this->config["cancel_url"] );
		if ($this->processed && $this->success && isset($this->config["success_url"])) // success
			$this->rh->Redirect( $this->config["success_url"] );
	}

	// вставка в БД
	function DbInsert()
	{
		if (!$this->config["db_table"]) 
			if ($this->config["db_ignore"]) return;
			else $this->rh->debug->Error("[Form]: *db_table* form config option is not set.");

		$fields = array(); 
		$values = array();
		foreach($this->fields as $k=>$v)
			$this->fields[$k]->DbInsert( $fields, $values );

		$this->_DbAuto( $fields, $values, true );

		foreach($values as $k=>$v)
			$values[$k] = $this->rh->db->Quote($values[$k]);

		$sql = "insert into ".$this->config["db_table"];
		if (sizeof($fields) > 0) 
			$sql.=" (".implode(",",$fields).") VALUES (".implode(",",$values).")";

		//$this->rh->debug->Error( $sql );
		$this->data_id = $this->rh->db->Insert($sql);

		foreach($this->fields as $k=>$v)
			$this->fields[$k]->DbAfterInsert( $this->data_id );
	}
	function DbUpdate( $data_id = NULL )
	{
		if (!$this->config["db_table"]) 
			if ($this->config["db_ignore"]) return;
			else $this->rh->debug->Error("[Form]: *db_table* form config option is not set.");

		if ($data_id == NULL) $data_id = $this->data_id;

		$fields = array(); 
		$values = array();
		foreach($this->fields as $k=>$v)
			$this->fields[$k]->DbUpdate( $data_id, $fields, $values );

		$this->_DbAuto( $fields, $values );

		$this->_DbUpdate( $fields, $values );

		foreach($this->fields as $k=>$v)
			$this->fields[$k]->DbAfterUpdate( $data_id );
	}
	function _DbUpdate ( &$fields, &$values )
	{
		$fields_values = array();
		foreach($fields as $k=>$v)
			$fields_values[] = $v." = ".$this->rh->db->Quote($values[$k]);

		$sql = "update ".$this->config["db_table"].
			" set ".implode(",",$fields_values)." where ".
			$this->config["id_field"]."=".$this->rh->db->Quote($this->data_id);
		//$this->rh->debug->Error( $sql );
		if (sizeof($fields) == 0) return false;
		$this->rh->db->Query($sql);

	}
	function _DbAuto( &$fields, &$values, $is_insert=false )
	{
		$user = $this->rh->principal->data["user_id"];
		$dt = date("Y-m-d H:i:s");
		if ($this->config["auto_user_id"])
		{
			if ($is_insert)
			{
				$fields[] = $this->config["fieldname_created_user_id"];//"_created_user_id";
				$values[] = $user;
			}
			$fields[] = $this->config["fieldname_edited_user_id"];//"_edited_user_id";
			$values[] = $user;
		}
		if ($this->config["auto_datetime"])
		{
			if ($is_insert)
			{
				$fields[] = $this->config["fieldname_created_datetime"];//"_created_datetime";
				$values[] = $dt;
			}
			$fields[] = $this->config["fieldname_edited_datetime"];//"_edited_datetime";
			$values[] = $dt;
		}
	}

	// загрузка из БД
	function Load( $data_id = NULL )  
	{
		if (!$this->config["db_table"])
			if ($this->config["db_ignore"]) return;
			else $this->rh->debug->Error("[Form]: *db_table* form config option is not set.");

		if ($data_id == NULL) $data_id = $this->data_id;
		$sql = "select * from ".$this->config["db_table"]." where ".
			$this->config["id_field"]."=".$this->rh->db->Quote($data_id);
		$data = $this->rh->db->QueryOne( $sql );
		if ($data == false) 
		{
			$this->data_id = 0;
			return;
		}
		foreach($this->fields as $k=>$v)
			$this->fields[$k]->DbLoad( $data );
	}

	// удаление из БД
	function DbDelete( $data_id = NULL )
	{
		if (!$this->config["db_table"]) 
			if ($this->config["db_ignore"]) return;
			else $this->rh->debug->Error("[Form]: *db_table* form config option is not set.");

		if ($data_id == NULL) $data_id = $this->data_id;
		foreach($this->fields as $k=>$v)
			$this->fields[$k]->DbDelete( $data_id );

		$sql = "delete from ".$this->config["db_table"]." where ".
			$this->config["id_field"]."=".$this->rh->db->Quote($data_id);
		$this->rh->db->Query( $sql );
	}

	// загрузка из массива
	function LoadFromArray( $a )
	{
		foreach($this->fields as $k=>$v)
			$this->fields[$k]->LoadFromArray( $a );
	}

	// ставим форме в соответствие строку в БД
	function AssignId( $data_id )
	{
		$this->data_id = $data_id;
	}


	var $_inner_name_counter = 0;
	function _NextInnerName()
	{
		$this->_inner_name_counter++;
		return "__inner_".$this->_inner_name_counter;
	}

	// EOC{ Form }
}


?>
